using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;

namespace Framework.Core.Xml
{
    public class XmlComparer : IXmlComparer
    {
        private static readonly XElementGroupingEqualityComparer XElementGroupingEqualityComparer =
            new XElementGroupingEqualityComparer();

        public IEnumerable<ComparisonResult> Compare(XDocument expected, XDocument actual)
        {
            PreCondition(expected, "expected");
            PreCondition(actual, "actual");

            PreCondition(expected.Root, "expected root element");
            PreCondition(actual.Root, "actual root element");

            var comparisonResults = new List<ComparisonResult>();

            var normalizedExpected = expected.Normalize();
            var normalizedActual = actual.Normalize();

            var expectedElement = normalizedExpected.Root;
            var actualElement = normalizedActual.Root;

            CompareElements(comparisonResults, new List<XElement> {expectedElement}, new List<XElement> {actualElement});

            return comparisonResults;
        }

        private static void CompareElements(ICollection<ComparisonResult> comparisonResults,
                                            IList<XElement> expectedElements,
                                            IList<XElement> actualElements)
        {
            CompareElements(comparisonResults, (IEnumerable<XElement>) expectedElements, actualElements);

            var max = GetMax(expectedElements, actualElements);

            for (var i = 0; i < max; i++)
            {
                var childListExpectedElement = EnsureEmptyList(expectedElements.ElementAtOrDefault(i));

                var childListActualElement = EnsureEmptyList(actualElements.ElementAtOrDefault(i));

                CompareElements(comparisonResults, childListExpectedElement, childListActualElement);
            }
        }

        private static IList<XElement> EnsureEmptyList(XContainer xContainer)
        {
            return (xContainer == null)
                       ? new List<XElement>()
                       : xContainer.Elements().ToList();
        }

        private static void CompareElements(ICollection<ComparisonResult> comparisonResults,
                                            IEnumerable<XElement> expectedElements,
                                            IEnumerable<XElement> actualElements)
        {
            var actualGroup = GroupByFirstDepth(actualElements);
            var expectedGroup = GroupByFirstDepth(expectedElements);

            var lowerToCompare = (expectedGroup.Count > actualGroup.Count) ? actualGroup : expectedGroup;
            var upperToCompare = (expectedGroup.Count > actualGroup.Count) ? expectedGroup : actualGroup;

            var intersect = lowerToCompare.Intersect(upperToCompare, XElementGroupingEqualityComparer).ToList();
            var differences = upperToCompare.Except(intersect, XElementGroupingEqualityComparer).ToList();

            foreach (var difference in differences)
            {
                var first = difference.First();

                if (EqualsButMissingDuplicates(expectedGroup, actualGroup, difference) ||
                    expectedGroup.Contains(difference, XElementGroupingEqualityComparer))
                {
                    comparisonResults.Add(new ComparisonResult(first.Name.LocalName, first, null));
                }

                else
                {
                    comparisonResults.Add(new ComparisonResult(first.Name.LocalName, null, first));
                }
            }
        }

        private static bool EqualsButMissingDuplicates(IEnumerable<IGrouping<int, XElement>> expected,
                                                       IEnumerable<IGrouping<int, XElement>> actual,
                                                       IGrouping<int, XElement> difference)
        {
            var expectedFirstContains = expected.FirstOrDefault(x => x.Key == difference.Key);
            var actualFirstContains = actual.FirstOrDefault(x => x.Key == difference.Key);

            return expectedFirstContains != null && actualFirstContains != null &&
                   expectedFirstContains.Count() > actualFirstContains.Count();
        }

        private static IList<IGrouping<int, XElement>> GroupByFirstDepth(IEnumerable<XElement> lower)
        {
            return
                lower.Select(
                    x =>
                    x.HasElements ? new XElement(x.Name, x.Attributes()) : new XElement(x.Name, x.Attributes(), x.Value))
                    .ToList().GroupBy(XElementGroupingEqualityComparer.GetHash).ToList();
        }

        private static int GetMax<T>(IList<T> expectedList, IList<T> actualList)
        {
            return (expectedList.Count() > actualList.Count())
                       ? expectedList.Count()
                       : actualList.Count();
        }

        // ReSharper disable UnusedParameter.Local
        private static void PreCondition(object o, string argumentName)
            // ReSharper restore UnusedParameter.Local
        {
            if (o == null)
            {
                throw new ArgumentNullException(string.Format("{0} can't be null", argumentName));
            }
        }
    }
}